/***************************************************************************
 *   oboe.math.Mesh3D							  *
 *   Copyright (C) 2011 by Felipe Manga					*
 *   lufeboma@gmail.com							*
 *									 *
 *   Permission is hereby granted, free of charge, to any person obtaining *
 *   a copy of this software and associated documentation files (the	   *
 *   "Software"), to deal in the Software without restriction, including   *
 *   without limitation the rights to use, copy, modify, merge, publish,   *
 *   distribute, sublicense, and/or sell copies of the Software, and to	*
 *   permit persons to whom the Software is furnished to do so, subject to *
 *   the following conditions:						 *
 *									 *
 *   The above copyright notice and this permission notice shall be	*
 *   included in all copies or substantial portions of the Software.	   *
 *									 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,	   *
 *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF	*
 *   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. *
 *   IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR	 *
 *   OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, *
 *   ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR *
 *   OTHER DEALINGS IN THE SOFTWARE.					   *
 ***************************************************************************/

package oboe.math;

import flash.utils.Dictionary;

import oboe.controllers.SceneController;
import oboe.core.Oboe;
import oboe.core.RenderSettings;

import flash.Vector;

class Mesh3D extends Mesh2D
{
private var uv:Vector<Float>;
private var frames3D:Vector< Vector.<Number3D> >;
private var indices:Vector<Int>;
private static var zscratch:Vector<Float> = new Vector();

private static inline var tBuffer:Vector<Triangle3D> = new Vector();
private static inline var outTBuffer:Array = [];
private var cache:Dictionary;

private static inline var pa:Float3D = new Number3D(0,0,0);
private static inline var pb:Float3D = new Number3D(0,0,0);
private static inline var pc:Float3D = new Number3D(0,0,0);
private static inline var tmp:Float3D = new Number3D(0,0,0);
private static inline var tmpA:Float3D = new Number3D(0,0,0);
private static inline var tmpB:Float3D = new Number3D(0,0,0);

private var curFrameTris:Vector<Float> = new Vector();

public function new( indices:Vector<Int>, mesh:Vector< Vector.<Number3D> >, uvs:Vector<Float> )
{
	super(null);
	this.uv = uvs;
	this.frames3D = mesh;
	if( !indices && mesh && mesh.length )
	{
	indices = new Vector<Int>( mesh[0].length, true );
	var l:Int = indices.length;
	for( var i:Int=0; i<l; ++i )
	{
		indices[i] = i;
	}
	}
	this.indices = indices;
	this.cache = new Dictionary();
}

public function forEachVertex( s:RenderSettings, func:Function ):Void
{
	if( s.frame > frames3D.length ) return;
	if( frames3D[ int(s.frame) ].length <= i ) return;
	var frame:Vector<Number3D> = frames3D[ int(s.frame) ];
	var l:Int = frame.length;
	
	for( var i:Int=0; i<l; ++i )
	{
	func( frame[i], i );
	}
}

public function getIndexFromApproxUV( u:Float, v:Float ):Int
{
	var vc:Int=uv.length;
	var pd:Float=0xFFFF;
	var lvp:Int=-1;
	var tud:Float;
	var tvd:Float;
	var td:Float;
	u -= int(u);
	v -= int(v);
	for( var i:Int=0; i<vc; i+=2 )
	{
	tud = uv[ i ];
	tud -= int(tud) + u;
	tvd = uv[ i+1 ];
	tvd -= int(tvd) + v;
	td = tud*tud+tvd*tvd;
	if( td<pd )
	{
		pd = td;
		lvp = i;
	}
	}
	return lvp>>1;
}

private static inline var tempRot:Rotator = new Rotator(0,0,0);
public function getTransformedVertex( s:RenderSettings, i:Int ):Float3D
{
	if( s.frame > frames3D.length ) return null;
	if( frames3D[ int(s.frame) ].length <= i ) return null;

	tempRot.copy3( s.rotation );
	tempRot.update();
	
	tmp.copy3( frames3D[ int(s.frame) ][ i ] );
	tmp.mul3( s.scale );
	tempRot.apply( tmp );
	tmp.add3( s.position );
	return tmp;
}

public function getVertex( s:RenderSettings, i:Int ):Float3D
{
	if( s.frame > frames3D.length ) return null;
	if( frames3D[ int(s.frame) ].length <= i ) return null;
	tmp.copy3( frames3D[ int(s.frame) ][ i ] );
	return tmp;
}

public override function draw( s:RenderSettings ):Void
{
	if( s.frame > frames3D.length ) return;
	
	var cacheKey:String = s.getCacheKey();
	this.frames = this.cache[ cacheKey ];
	if( !this.frames )
	{
	if( s.debugCache )
	{
		trace("Cache miss on ", cacheKey);
	}
	this.frames = new Vector<Frame>(frames3D.length, true);
	if( s.mode&RenderSettings.MODE_CACHE_ONLY )
	{
		this.cache[ cacheKey ] = this.frames;			
	}
	}
	
	if( !this.frames[ int(s.frame) ] )
	{
	while( indices.length > tBuffer.length )
	{
		tBuffer.push( new Triangle3D() );
	}
	outTBuffer.length = tBuffer.length;
	var frame3D:Vector<Number3D> = frames3D[ int(s.frame) ];
	var verts:Vector<Float> = new Vector<Float>( frame3D.length*2, true );
	var uvs:Vector<Float> = this.uv;
	outTBuffer.length = this.renderFrame( frame3D, verts, null, s );
	var frame2D:Frame = new Frame( outTBuffer, verts, uvs );
	this.frames[ int(s.frame) ] = frame2D;
	}

	switch( s.projection )
	{
	case RenderSettings.PROJECT_PARALLEL:
		s.containerShiftOut.set( s.position.x-SceneController._camera_position.x, -s.position.y+SceneController._camera_position.y );
		break;
	case RenderSettings.PROJECT_ISOMETRIC:
		tmp.copy3( s.position ).sub3( SceneController._camera_position );
		s.containerShiftOut.set( -tmp.x+tmp.z, -tmp.y + (tmp.x+tmp.z)*0.5 );
		break;
	case RenderSettings.PROJECT_PERSPECTIVE:
		s.containerShiftOut.set( 0, 0 );
		break;
	}
	
	s.containerShiftOut.add2( s.containerShiftIn );
	
	super.draw( s );
}

private function renderFrame( frame:Vector<Number3D>, verts:Vector<Float>, uvs:Vector<Float>, s:RenderSettings ):Int
{
	var tbp:Int=0;
	if( !s.rotation.isEqual( s.rotator ) )
	{
	s.rotator.copy3( s.rotation );
	s.rotator.update();
	}
	
	if( SceneController._camera_position )
	{
	RenderSettings.finalPosition.copy3( s.position ).sub3( SceneController._camera_position );
	}
	else
	{
	RenderSettings.finalPosition.copy3( s.position );
	}
	
	var maxScreenX:Float;
	var maxScreenY:Float;
	var screenCulling:Bool = false;
	var transform:Function;
	
	if( s.projection == RenderSettings.PROJECT_PERSPECTIVE )
	{
	maxScreenX = Oboe.stage.stageWidth/2;
	maxScreenY = Oboe.stage.stageHeight/2;
	screenCulling = true;
	transform = this.transformPerspective;
	}
	else if( s.projection == RenderSettings.PROJECT_ISOMETRIC )
	{
	transform = this.transformIsometric;
	}
	else
	{
	transform = this.transformParallel;
	}

	var cv:Int;
	var iid:Int;
	var length:Int;
	iid=0;
	
	if( zscratch.length < frame.length )
	{
	zscratch.length = frame.length;
	}
	transform( frame, verts, s );
	
	var tri:Triangle3D;
	length = indices.length;
	switch( s.culling )
	{
	case RenderSettings.CULL_NONE:
	for( iid=0; iid<length; iid+=3 )
	{
		pa.z = zscratch[ indices[iid  ] ];
		pb.z = zscratch[ indices[iid+1] ];
		pc.z = zscratch[ indices[iid+2] ];
		tri = tBuffer[tbp];
		tri.pointA = indices[iid];
		tri.pointB = indices[iid+1];
		tri.pointC = indices[iid+2];
		tri.z = pa.z+pb.z+pc.z;
		outTBuffer[tbp++] = tri;
	}
	break;
	case RenderSettings.CULL_BACK:
	for( iid=0; iid<length; iid+=3 )
	{
		cv = indices[iid  ]; pa.z = zscratch[ cv ]; cv <<= 1; pa.x = verts[ cv++ ]; pa.y = verts[ cv ];
		cv = indices[iid+1]; pb.z = zscratch[ cv ]; cv <<= 1; pb.x = verts[ cv++ ]; pb.y = verts[ cv ];
		cv = indices[iid+2]; pc.z = zscratch[ cv ]; cv <<= 1; pc.x = verts[ cv++ ]; pc.y = verts[ cv ];
		
		tmpA.x = pa.x - pb.x;
		tmpA.y = pa.y - pb.y;
		tmpB.x = pa.x - pc.x;
		tmpB.y = pa.y - pc.y;
		tmp.z = tmpA.x*tmpB.y-tmpA.y*tmpB.x;
		
		if( tmp.z < 0 )
		{
		tri = tBuffer[tbp];
		tri.pointA = indices[iid];
		tri.pointB = indices[iid+1];
		tri.pointC = indices[iid+2];
		tri.z = pa.z+pb.z+pc.z;
		outTBuffer[tbp++] = tri;
		}
	}
	break;
	case RenderSettings.CULL_FRONT:
	for( iid=0; iid<length; iid+=3 )
	{
		cv = indices[iid  ]; pa.z = zscratch[ cv ]; cv <<= 1; pa.x = verts[ cv++ ]; pa.y = verts[ cv ];
		cv = indices[iid+1]; pb.z = zscratch[ cv ]; cv <<= 1; pb.x = verts[ cv++ ]; pb.y = verts[ cv ];
		cv = indices[iid+2]; pc.z = zscratch[ cv ]; cv <<= 1; pc.x = verts[ cv++ ]; pc.y = verts[ cv ];
		tmpA.x = pa.x - pb.x;
		tmpA.y = pa.y - pb.y;
		tmpB.x = pa.x - pc.x;
		tmpB.y = pa.y - pc.y;
		tmp.z = tmpA.x*tmpB.y-tmpA.y*tmpB.x;
		
		if( tmp.z > 0 )
		{
		tri = tBuffer[tbp];
		tri.pointA = indices[iid];
		tri.pointB = indices[iid+1];
		tri.pointC = indices[iid+2];
		tri.z = pa.z+pb.z+pc.z;
		outTBuffer[tbp++] = tri;
		}
	}
	break;
	}
	return tbp;
}

private function transformParallel( frame:Vector<Number3D>, verts:Vector<Float>, s:RenderSettings ):Void // vert:Float3D, out:Float3D, s:RenderSettings ):Void
{
	var iid:Int;
	var length:Int;
	var xvx : Float = s.rotator.xvx;
	var xvy : Float = s.rotator.xvy;
	var xvz : Float = s.rotator.xvz;
	var yvx : Float = s.rotator.yvx;
	var yvy : Float = s.rotator.yvy;
	var yvz : Float = s.rotator.yvz;
	var zvx : Float = s.rotator.zvx;
	var zvy : Float = s.rotator.zvy;
	var zvz : Float = s.rotator.zvz;
	var vert:Float3D;
	var z:Float;
	var cv:Int;
	length=frame.length;
	iid=0;
	for( cv=0; cv<length; cv++ )
	{
	vert = frame[cv];
	// s.rotator.constapply( vert, tmp );
	tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
	tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
	tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
	
	tmp.x = tmp.x * s.scale.x;
	tmp.y = tmp.y * s.scale.y;
	tmp.z = tmp.z * s.scale.z;
	
	verts[ iid++ ] = tmp.x;
	verts[ iid++ ] =  -tmp.y;
	zscratch[cv] =  tmp.z;
	}
	
}

private function transformIsometric( frame:Vector<Number3D>, verts:Vector<Float>, s:RenderSettings ):Void // vert:Float3D, out:Float3D, s:RenderSettings ):Void
{
	var iid:Int;
	var length:Int;
	var xvx : Float = s.rotator.xvx;
	var xvy : Float = s.rotator.xvy;
	var xvz : Float = s.rotator.xvz;
	var yvx : Float = s.rotator.yvx;
	var yvy : Float = s.rotator.yvy;
	var yvz : Float = s.rotator.yvz;
	var zvx : Float = s.rotator.zvx;
	var zvy : Float = s.rotator.zvy;
	var zvz : Float = s.rotator.zvz;
	var vert:Float3D;
	var z:Float;
	var cv:Int;
	length=frame.length;
	iid=0;
	for( cv=0; cv<length; cv++ )
	{
	vert = frame[cv];
	// s.rotator.constapply( vert, tmp );
	tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
	tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
	tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
	
	tmp.x = tmp.x * s.scale.x;
	tmp.y = tmp.y * s.scale.y;
	tmp.z = tmp.z * s.scale.z;
	
	verts[ iid++ ] = tmp.x - tmp.z;
	verts[ iid++ ] =  -tmp.y + (tmp.x+tmp.z)*0.5;
	zscratch[cv] =  - tmp.z - tmp.x;
	}
}

private function transformPerspective( frame:Vector<Number3D>, verts:Vector<Float>, s:RenderSettings ):Void // vert:Float3D, out:Float3D, s:RenderSettings ):Void
{
	var iid:Int;
	var length:Int;
	var xvx : Float = s.rotator.xvx;
	var xvy : Float = s.rotator.xvy;
	var xvz : Float = s.rotator.xvz;
	var yvx : Float = s.rotator.yvx;
	var yvy : Float = s.rotator.yvy;
	var yvz : Float = s.rotator.yvz;
	var zvx : Float = s.rotator.zvx;
	var zvy : Float = s.rotator.zvy;
	var zvz : Float = s.rotator.zvz;
	var vert:Float3D;
	var z:Float;
	var cv:Int;
	length=frame.length;
	iid=0;
	if( SceneController._camera_rotator )
	{
	for( cv=0; cv<length; cv++ )
	{
		vert = frame[cv];
		// s.rotator.constapply( vert, tmp );
		tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
		tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
		tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
		
		tmp.x = tmp.x * s.scale.x + RenderSettings.finalPosition.x;
		tmp.y = tmp.y * s.scale.y + RenderSettings.finalPosition.y;
		tmp.z = tmp.z * s.scale.z + RenderSettings.finalPosition.z;

		SceneController._camera_rotator.apply( tmp );
		
		z = s.focalLength / (s.focalLength + tmp.z);
		verts[ iid++ ] = tmp.x * z;
		verts[ iid++ ] = -tmp.y * z;
		zscratch[cv] = tmp.z;
	}
	}
	else
	{
	for( cv=0; cv<length; cv++ )
	{
		vert = frame[cv];
		// s.rotator.constapply( vert, tmp );
		tmp.x = vert.x * xvx + vert.y * xvy + vert.z * xvz;
		tmp.y = vert.x * yvx + vert.y * yvy + vert.z * yvz;
		tmp.z = vert.x * zvx + vert.y * zvy + vert.z * zvz;
		
		tmp.x = tmp.x * s.scale.x + RenderSettings.finalPosition.x;
		tmp.y = tmp.y * s.scale.y + RenderSettings.finalPosition.y;
		tmp.z = tmp.z * s.scale.z + RenderSettings.finalPosition.z;
		
		z = s.focalLength / (s.focalLength + tmp.z);
		verts[ iid++ ] = tmp.x * z;
		verts[ iid++ ] = -tmp.y * z;
		zscratch[cv] = tmp.z;
	}
	}
}
}